核心概念

# 核心概念

[TOC]

# 一、异步I/O

Node将异步作为主要编程和设计理念,可以更好的并发。

  • 利用单线程,远离多线程死锁、状态同步等问题。
  • 利用异步I/O,让单线程远离阻塞,以更好地使用CPU。

# 1.1 不同的I/O类型对应的开销

I/O类型 花费的CPU时钟周期
CPU一级缓存
CPU二级缓存 14
内存 250
硬盘 41000000
网络 240000000
  • I/O是昂贵的,分布式I/O是更昂贵的。
  • 只有后端能够快速响应资源,才能让前端的体验好。

# 1.2 异步I/O的调用

  • I/O的调用不再阻塞后续运算,将原有等待I/O完成的这段时间分配给其余需要的业务去执行。

io-execute

# 1.3 阻塞与非阻塞

操作系统内核对于I/O只有两种方式:阻塞与非阻塞。

# 1.3.1 阻塞I/O

  • 在调用阻塞I/O时,应用程序需要等待I/O完成才返回结果。
  • 特点:调用之后一定要等到系统内核层面完成所有操作后,调用才结束。
  • 缺点:造成CPU等待I/O,浪费等待时间,CPU处理能力不能得到充分利用。

# 1.3.2 非阻塞I/O

  • 非阻塞I/O不带数据直接返回,要获取数据,还需要通过文件描述符再次读取。
  • 非阻塞I/O返回之后,CPU的时间片可以用来处理其他事务,性能得到提升。
  • 缺点:为了获取完整数据,应用程序需要通过轮询来确认是否完成。

# 1.3.3 Node的阻塞与非阻塞

  • 阻塞方法同步执行,非阻塞方法异步执行。
// 读取的同步文件
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // blocks here until file is read
1
2
3
// 等效的异步读取
// fs.readFile()是非阻塞的
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
});
1
2
3
4
5
6
  • 避免混合阻塞和非阻塞代码的危险
// fs.unlinkSync()可能在fs.readFile()之前执行
// 即file.md在实际读取之前删除
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
fs.unlinkSync('/file.md');
1
2
3
4
5
6
7
8
// 在回调中放置非阻塞调用,保证正确的操作顺序
const fs = require('fs');
fs.readFile('/file.md', (readFileErr, data) => {
  if (readFileErr) throw readFileErr;
  console.log(data);
  fs.unlink('/file.md', (unlinkErr) => {
    if (unlinkErr) throw unlinkErr;
  });
});
1
2
3
4
5
6
7
8
9

# 二、事件循环

事件循环允许Node.js执行非阻塞I / O操作 。

  • 在node中,除了JavaScript是单线程外,Node自身其实是多线程的,只是I/O线程使用的CPU较少。
  • 除了用户代码无法并行执行外,所有的I/O(磁盘I/O和网络I/O等)则是可以并行起来的。
  • 整个异步I/O的流程如下:

IO-process

# 三、非I/O的异步API

# 3.1 定时器

setTimeout

# 3.2 process.nextTick()

  • 可以实现立即异步执行一个任务。
process.nextTick(fn)

// 类似于
setTimeout(function () {
    // TODO
}, 0)
1
2
3
4
5
6

setTimeout(fn,0)较为浪费性能,需要动用红黑树、创建定时器对象和迭代等操作;并且由于事件循环自身的特点,定时器的精确度不够。相比之下,process.nextTick()操作较为轻量。

# 3.3 setImmediate()

类似于process.nextTick(),都是将回调函数延迟执行。

setImmediate(fn)
1

# 3.3.1 与process.nextTick()的异同

  1. 执行顺序:process.nextTick()优于setImmdiate()
setImmediate(() => {
  console.log(`setImmediate1`)
})
process.nextTick(() => {
  console.log(`nextTick1`)
})
console.log('console')

// console
// nextTick1
// setImmediate1
1
2
3
4
5
6
7
8
9
10
11

process.nextTick()属于idle观察者,setImmediate()属于check观察者。在每一轮轮询检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。

  1. 具体实现:process.nextTick()的回调函数保存在一个数组中,setImmdiate()的结果则保存在链表中。
  2. 行为:process.nextTick()再每轮循环中会将数组中的回调函数全部执行完;而setImmdiate()在每轮循环中执行链表中的一个回调函数,可保证每轮循环能够较快执行结束,防止CPU占用过多而阻塞后续I/O调用的情况。